use reqwest::Client;
use secrecy::{ExposeSecret, SecretString};
use serde::Serialize;
use std::time::Duration;

use crate::domain::SubscriberEmail;

pub struct EmailClient {
    // 发件人
    sender: SubscriberEmail,
    // reqwest::Client 实例
    http_client: Client,
    // 发送 API 请求的 URL
    base_url: String,
    path: String,
    api_user: SecretString,
    api_key: SecretString,
}

impl EmailClient {
    pub fn new(
        sender: SubscriberEmail,
        base_url: &str,
        path: &str,
        api_user: SecretString,
        api_key: SecretString,
        timeout: Duration,
    ) -> Self {
        Self {
            sender,
            http_client: Client::builder().timeout(timeout).build().unwrap(),
            base_url: base_url.to_owned(),
            path: path.to_owned(),
            api_user,
            api_key,
        }
    }
    pub async fn send_email(
        &self,
        recipient: &SubscriberEmail,
        subject: &str,
        html_content: &str,
        text_content: &str,
    ) -> Result<(), reqwest::Error> {
        let request_body = SendEmailRequest {
            from: self.sender.as_ref(),
            to: recipient.as_ref(),
            subject,
            html: html_content,
            text: text_content,
            api_user: &self.api_user,
            api_key: &self.api_key,
        };
        let url = format!("{}{}", self.base_url, self.path);
        let builder = self
            .http_client
            .post(url)
            .body(serde_qs::to_string(&request_body).unwrap())
            .header("Content-Type", "application/json");
        builder.send().await?.error_for_status().map(|_| ())
    }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SendEmailRequest<'a> {
    from: &'a str,
    to: &'a str,
    subject: &'a str,
    html: &'a str,
    text: &'a str,
    // 仅在序列化时取出其真实值？
    #[serde(serialize_with = "serialize_secret_string")]
    api_user: &'a SecretString,
    #[serde(serialize_with = "serialize_secret_string")]
    api_key: &'a SecretString,
}

// 自定义序列化方法
fn serialize_secret_string<S>(value: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    serializer.serialize_str(value.expose_secret())
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use super::*;
    use crate::domain::SubscriberEmail;
    use claim::{assert_err, assert_ok};
    use fake::faker::internet::en::SafeEmail;
    use fake::faker::lorem::en::{Paragraph, Sentence};
    use fake::{Fake, Faker};
    use wiremock::matchers::{any, header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    struct SendEmailBodyMatcher;
    impl wiremock::Match for SendEmailBodyMatcher {
        fn matches(&self, request: &wiremock::Request) -> bool {
            let body = String::from_utf8_lossy(&request.body);
            let kv_body = body
                .split('&')
                .map(|kv| {
                    let mut tmp = kv.split('=');
                    (tmp.next(), tmp.next())
                })
                .filter(|(k, v)| k.is_some() && v.is_some())
                .map(|(k, v)| (k.unwrap(), v.unwrap()))
                .collect::<BTreeMap<_, _>>();
            kv_body.contains_key("apiUser")
                && kv_body.contains_key("apiKey")
                && kv_body.contains_key("to")
                && kv_body.contains_key("from")
                && kv_body.contains_key("subject")
                && kv_body.contains_key("html")
        }
    }

    /// 生成邮件主题
    fn subject() -> String {
        Sentence(1..2).fake()
    }
    /// 生成邮件内容
    fn content() -> String {
        Paragraph(1..10).fake()
    }
    /// 生成订阅者邮件地址
    fn email() -> SubscriberEmail {
        SubscriberEmail::parse(SafeEmail().fake()).unwrap()
    }
    /// 生成 EmailClient 实例
    fn email_client(base_url: &str, path: &str) -> EmailClient {
        EmailClient::new(
            SubscriberEmail::parse(SafeEmail().fake()).unwrap(),
            base_url,
            path,
            Faker.fake::<String>().into(),
            Faker.fake::<String>().into(),
            std::time::Duration::from_millis(100),
        )
    }
    #[tokio::test]
    async fn send_email_sends_the_expected_request() {
        let mock_server = MockServer::start().await;
        let endpoint = format!("/{}", Faker.fake::<String>());

        Mock::given(header("Content-Type", "application/json"))
            .and(path(&endpoint))
            .and(method("POST"))
            .and(SendEmailBodyMatcher)
            .respond_with(ResponseTemplate::new(200))
            .expect(1)
            .mount(&mock_server)
            .await;

        let email_client = email_client(&mock_server.uri(), &endpoint);
        let _ = email_client
            .send_email(&email(), &subject(), &content(), &content())
            .await;

        // 断言：Mock会在析构时检查断言
    }

    #[tokio::test]
    async fn send_email_succeeds_if_the_server_returns_200() {
        let mock_server = MockServer::start().await;
        let endpoint = format!("/{}", Faker.fake::<String>());

        Mock::given(any())
            .respond_with(ResponseTemplate::new(200))
            .expect(1)
            .mount(&mock_server)
            .await;

        let email_client = email_client(&mock_server.uri(), &endpoint);
        let response = email_client
            .send_email(&email(), &subject(), &content(), &content())
            .await;

        // 断言：Mock会在析构时检查断言
        assert_ok!(response);
    }

    #[tokio::test]
    async fn send_email_failed_if_the_server_returns_500() {
        let mock_server = MockServer::start().await;
        let endpoint = format!("/{}", Faker.fake::<String>());

        Mock::given(any())
            .respond_with(ResponseTemplate::new(500))
            .expect(1)
            .mount(&mock_server)
            .await;

        let email_client = email_client(&mock_server.uri(), &endpoint);
        let response = email_client
            .send_email(&email(), &subject(), &content(), &content())
            .await;

        // 断言：Mock会在析构时检查断言
        assert_err!(response);
    }

    #[tokio::test]
    async fn send_email_times_out_if_the_server_takes_too_long() {
        let mock_server = MockServer::start().await;
        let endpoint = format!("/{}", Faker.fake::<String>());

        let response = ResponseTemplate::new(500).set_delay(std::time::Duration::from_secs(180));
        Mock::given(any())
            .respond_with(response)
            .expect(1)
            .mount(&mock_server)
            .await;

        let email_client = email_client(&mock_server.uri(), &endpoint);
        let response = email_client
            .send_email(&email(), &subject(), &content(), &content())
            .await;

        // 断言：Mock会在析构时检查断言
        assert_err!(response);
    }
}
